package utils;

import java.util.Stack;
import java.util.StringTokenizer;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.math.MathException;
import org.apache.commons.math.util.DefaultTransformer;

import enums.Operator;

/**
 * Contains method to calculate formulas.
 * 
 * @author Marcus
 * 
 */
public class FormulaCalculator {

	private String delimeters;

	private DefaultTransformer mathematicalTransformer = new DefaultTransformer();

	/**
	 * Public constructor.
	 */
	public FormulaCalculator() {
		createDelimeters();
	}

/**
	 * Returns the used {@link DefaultTransformer].
	 * 
	 * @return mathematical transformer
	 */
	private DefaultTransformer getMathematicalTransformer() {
		return this.mathematicalTransformer;
	}

	/**
	 * Creates the delimiters needed for the tokenizer.
	 */
	private void createDelimeters() {
		String del = " " + ",";
		for (Operator operator : Operator.values()) {
			del = del.concat(operator.getOperatorString());
		}

		this.delimeters = del;
	}

	/**
	 * Calculates the passed formula.
	 * 
	 * @param formula
	 *            formula which is calculated
	 * @param variables
	 *            needed values for the variables
	 * 
	 * @return result of the calculation
	 */
	public double calculateFormula(String formula, int... variables) {
		StringTokenizer tokenizer = new StringTokenizer(formula.toUpperCase(),
				this.delimeters, true);
		Stack<Double> numberStack = new Stack<Double>();
		Stack<Operator> operatorStack = new Stack<Operator>();

		while (tokenizer.hasMoreTokens()) {
			String token = evaluateToken(tokenizer.nextToken(), variables);

			if (token != null && !token.isEmpty()) {
				Operator operator = Operator.getOperatorEnum(token);
				// If the token is an operator it is verified whether the
				// operator must be resolved immediately or late.
				if (operator != null) {
					// TODO: Add power calculation
					// TODO: Add bracket handling
					if (operator.getPriority() > 0) {
						performImediateOperation(tokenizer, numberStack,
								operator, variables);
					} else {
						operatorStack.add(operator);
					}
				} else {
					numberStack.add(Double
							.valueOf(transformStringToNumber(token)));
				}
			}
		}

		// Perform the remaining operations

		while (!operatorStack.isEmpty()) {
			// The first number is the second variable in the operation.
			double number2 = numberStack.pop().doubleValue();
			double number1 = numberStack.pop().doubleValue();
			Operator operator = operatorStack.pop();

			double result = operator.performOperation(number1, number2);
			numberStack.push(Double.valueOf(result));
		}

		if (numberStack.size() != 1) {
			throw new IllegalStateException(
					"Unexpected number of remaining numbers in the stack after all operations were performed.");
		}
		double result = numberStack.pop().doubleValue();
		return result;
	}

	/**
	 * Transforms the passed token into a {@code double}.
	 * 
	 * @param token
	 *            token which is transformed
	 * 
	 * @return double value
	 */
	private double transformStringToNumber(String token) {
		double number;
		try {
			number = getMathematicalTransformer().transform(token);
		} catch (MathException mathexception) {
			throw new IllegalArgumentException("The token '" + token
					+ "' is not a number.");
		}
		return number;
	}

	/**
	 * Performs an operation which has to be resolved immediate.
	 * 
	 * @param tokenizer
	 *            used Tokenizer with the formula
	 * @param numberStack
	 *            stack which contains the actual numbers
	 * @param operator
	 *            operator which has the executed
	 * @param variables
	 *            needed values for variables
	 */
	private void performImediateOperation(StringTokenizer tokenizer,
			Stack<Double> numberStack, Operator operator, int... variables) {
		Double stackNumber = numberStack.pop();
		String tokenNumber = evaluateToken(tokenizer.nextToken(), variables);

		double number = transformStringToNumber(tokenNumber);
		double result = operator.performOperation(stackNumber.doubleValue(),
				number);

		numberStack.add(Double.valueOf(result));
	}

	/**
	 * Evaluates if the passed token is a variable and if yes swap the variable
	 * with the corresponding value.
	 * 
	 * @param token
	 *            token which is evaluated
	 * @param available
	 *            variables
	 * 
	 * @return evaluated token
	 */
	private String evaluateToken(String token, int... variables) {
		String evaluatetedToken = StringUtils.deleteWhitespace(token);
		if (evaluatetedToken.startsWith("X")) {
			String number = token.substring(1);
			int position = Integer.parseInt(number);
			evaluatetedToken = Integer.toString(variables[position]);
		}
		return evaluatetedToken;
	}
}
